package cn.jtfadmin.base.data.jpa.service;

import cn.jtfadmin.base.lang.common.domain.dto.*;
import cn.jtfadmin.base.lang.common.domain.entity.IdEntity;
import cn.jtfadmin.base.lang.common.service.AbstractBaseService;
import cn.jtfadmin.base.lang.common.service.BaseService;
import cn.jtfadmin.base.lang.common.utils.AkUtils;
import cn.jtfadmin.base.lang.common.utils.JsonUtils;
import cn.jtfadmin.base.lang.common.utils.ReflectionUtils;
import cn.jtfadmin.base.lang.config.ApplicationHolder;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.beans.PropertyDescriptor;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.*;
import java.util.stream.Collectors;

public abstract class JpaService<T extends IdEntity> extends AbstractBaseService<T> {
    @Override
    @Transactional
    protected T doSave(@NotNull T t) {
        T t1 = getJpaRepository().saveAndFlush(t);
        return t1;
    }

    @Override
    @Transactional
    protected void doSaveAll(@NotNull Iterable<T> iterable) {
       iterable.forEach(t -> t = doSave(t));
    }

    @Override
    public Optional<T> findById(@NotNull String id) {
        return getJpaRepository().findById(id);
    }

    @Override
    public List<T> findByIds(@NotNull Collection<String> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return Collections.emptyList();
        }
        return getJpaRepository().findAllById(ids);
    }







    @Override
    public List<T> findAll(@Nullable List<SpecConditionDTO> specConditions, @Nullable String orderColumn, @Nullable String order) {
        return findAll(specConditionToSpecification(specConditions), createSort(orderColumn, order));
    }


    /**
     * 查询所有
     * @param specConditions 查询条件
     * @param orderColumn 排序列
     * @param order 排序 desc/asc
     * @param pageNo pageNo
     * @param pageSize pageSize
     * @return
     */
    public  PageRDTO<T> findAll(@Nullable List<SpecConditionDTO> specConditions, @Nullable String orderColumn, @Nullable String order, int pageNo, int pageSize){
        Page<T> page = getJpaRepository().findAll(
                specConditionToSpecification(specConditions),
                PageRequest.of(pageNo - 1, pageSize, createSort(orderColumn, order))
        );
        return new PageRDTO<>(page.getContent(), page.getTotalElements());
    }


    protected Sort createSort(String column, String order){
        if (StringUtils.hasText(column)) {
            if (StringUtils.hasText(order)) {
                return Sort.by(Sort.Direction.valueOf(order.toUpperCase().trim()), StringUtils.delimitedListToStringArray(column, ","));
            } else {
                return Sort.by(Sort.Direction.ASC, StringUtils.delimitedListToStringArray(column, ","));
            }
        }
        return Sort.unsorted();
    }



    protected Path<?> getColumnPath(Root root, String column){
        Path<?> path = null;
        String[] columnArrayStrings = StringUtils.delimitedListToStringArray(column, ".");
        if (columnArrayStrings.length == 1) {
            path = root.get(columnArrayStrings[0].trim());
        } else {
            Join<?, ?> join = null;
            for (int i = 0; i < columnArrayStrings.length; i++) {
                if (i == 0) {
                    join = root.join(columnArrayStrings[0].trim());
                } else if (i != columnArrayStrings.length - 1) {
                    join = join.join(columnArrayStrings[i].trim());
                } else if (i == columnArrayStrings.length - 1) {
                    path = join.get(columnArrayStrings[i].trim());
                }
            }
        }
        return path;
    }

    protected Specification<T> specConditionToSpecification(Collection<SpecConditionDTO> specs){
        if(CollectionUtils.isEmpty(specs)){
            return Specification.where((root, query, criteriaBuilder) -> {query.distinct(true); return null;});
        }
        return (root, query, criteriaBuilder)->{
            query.distinct(true);
            // 不需要值的type
            Set<String> noValueTypes = new HashSet<>(Arrays.asList("isNull", "notNull", "isEmpty", "notEmpty", "isEmptyOrNull", "notEmptyOrNull"));
            // 需要值的type
            Set<String> valueTypes = new HashSet<>(Arrays.asList("in","notIn", "ne",  "eq", "ge", "gt", "le", "lt", "like", "likeLeft", "likeRight", "between"));
            // 所有的types
            Set<String> types = new HashSet<>(valueTypes);
            types.addAll(noValueTypes);

            List<Predicate> list = new ArrayList<>(specs.size());
            for (SpecConditionDTO spec : specs) {
                String type = spec.getType();
                String column = spec.getColumn();
                String value = spec.getValue();
                if(!types.contains(type)){
                    throw new IllegalArgumentException("查询条件类型错误,不支持"+type);
                }
                if(valueTypes.contains(type) && !StringUtils.hasText(value)){
                    continue;
                }
                Path<?> path = getColumnPath(root, column);
                Objects.requireNonNull(path);
                // null not null
                Class<?> conditionColumnType = path.getJavaType();
                switch (type) {
                    case "isNull":
                        list.add(criteriaBuilder.isNull(path));
                        break;
                    case "notNull":
                        list.add(criteriaBuilder.isNotNull(path));
                        break;
                    case "isEmpty":
                        if(Collection.class.isAssignableFrom(path.getJavaType())){
                            list.add(criteriaBuilder.isEmpty((Path<Collection>)path));
                        }
                        if(String.class.isAssignableFrom(path.getJavaType())){
                            list.add(criteriaBuilder.equal(path, ""));
                        }
                        break;
                    case "notEmpty":
                        if(Collection.class.isAssignableFrom(path.getJavaType())){
                            list.add(criteriaBuilder.isNotEmpty((Path<Collection>)path));
                        }
                        if(String.class.isAssignableFrom(path.getJavaType())){
                            list.add(criteriaBuilder.notEqual(path, ""));
                        }
                        break;
                    case "isEmptyOrNull":
                        if(Collection.class.isAssignableFrom(path.getJavaType())){
                            list.add(
                                    criteriaBuilder.or(
                                            criteriaBuilder.isEmpty((Path<Collection>)path),
                                            criteriaBuilder.isNull((Path<Collection>)path)
                                    )
                            );
                        }
                        if(String.class.isAssignableFrom(path.getJavaType())){
                            list.add(
                                    criteriaBuilder.or(
                                            criteriaBuilder.equal(path, ""),
                                            criteriaBuilder.isNull(path)
                                    )
                            );
                        }
                        break;
                    case "notEmptyOrNull":
                        if(Collection.class.isAssignableFrom(path.getJavaType())){
                            list.add(
                                    criteriaBuilder.or(
                                            criteriaBuilder.isNotEmpty((Path<Collection>)path),
                                            criteriaBuilder.isNotNull((Path<Collection>)path)
                                    )
                            );
                        }
                        if(String.class.isAssignableFrom(path.getJavaType())){
                            list.add(
                                    criteriaBuilder.or(
                                            criteriaBuilder.notEqual(path, ""),
                                            criteriaBuilder.isNotNull(path)
                                    )
                            );
                        }
                        break;
                    default://ignore
                }

                //in notin
                if ("in".equals(type) || "notIn".equals(type)) {
                    Set<String> valueSet = StringUtils.commaDelimitedListToSet(value);
                    CriteriaBuilder.In<Object> in = criteriaBuilder.in(path);
                    valueSet.forEach(v -> {
                        Object targetValue = ApplicationHolder.getApplicationContext().getBean(ConversionService.class)
                                .convert(v, conditionColumnType);
                        in.value(targetValue);
                    });
                    switch (type) {
                        case "in":
                            list.add(in);
                            break;
                        case "notIn":
                            list.add(criteriaBuilder.not(in));
                            break;
                        default:
                            //ignore
                    }
                }
                switch (type){
                    case "between":
                        String[] split = value.split(",");
                        list.add(criteriaBuilder.between((Expression<Comparable>)path, (Comparable)ApplicationHolder.getApplicationContext().getBean(ConversionService.class).convert(split[0], conditionColumnType),
                                (Comparable)ApplicationHolder.getApplicationContext().getBean(ConversionService.class).convert(split[1], conditionColumnType)));
                        break;
                }
                Object targetValue = ApplicationHolder.getApplicationContext().getBean(ConversionService.class).convert(value, conditionColumnType);
                switch (type) {
                    case "ne":
                        list.add(criteriaBuilder.notEqual(path, targetValue));
                        break;
                    case "eq":
                        list.add(criteriaBuilder.equal(path, targetValue));
                        break;
                    case "ge":
                        list.add(criteriaBuilder.greaterThanOrEqualTo((Expression<Comparable>) path, (Comparable) targetValue));
                        break;
                    case "gt":
                        list.add(criteriaBuilder.greaterThan((Expression<Comparable>) path, (Comparable) targetValue));
                        break;
                    case "le":
                        list.add(criteriaBuilder.lessThanOrEqualTo((Expression<Comparable>) path, (Comparable) targetValue));
                        break;
                    case "lt":
                        list.add(criteriaBuilder.lessThan((Expression<Comparable>) path, (Comparable) targetValue));
                        break;
                    case "like":
                        list.add(criteriaBuilder.like((Expression<String>) path, "%" + targetValue + "%"));
                        break;
                    case "likeLeft":
                        list.add(criteriaBuilder.like((Expression<String>) path, targetValue + "%"));
                        break;
                    case "likeRight":
                        list.add(criteriaBuilder.like((Expression<String>) path, "%" + targetValue));
                        break;

                    default:
                        // ignore
                }
            }
            if(list.isEmpty()){
                return null;
            }else {
                return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
            }
        };
    }

    protected Specification<T> specJsonToSpecification(String specJson){

        if (!StringUtils.hasText(specJson)) {
            return null;
        }
        // 重新反编码，因为get方法时参数在浏览器上
        String specJsonStr;
        try {
            specJsonStr = URLDecoder.decode(specJson, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("参数编码错误", e);
        }
        SpecConditionDTO[] specs = JsonUtils.constructArrayType(specJsonStr, SpecConditionDTO.class);
        return specConditionToSpecification(Arrays.asList(specs));
    }


    @Override
    @Transactional
    public void deleteBatch(@NotNull Collection<String> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return;
        }
        getJpaRepository().deleteAll(findByIds(ids));
    }




    /**
     * 获取所有
     * @param specification 条件
     * @param sort 排序
     * @return 结果
     */
    public List<T> findAll(@Nullable Specification<T> specification, @Nullable Sort sort){
        return getJpaRepository().findAll(specification,sort);
    }

    /**
     * 获取所有
     * @param specification 条件
     * @return 结果
     */
    public List<T> findAll(@Nullable Specification<T> specification){
        return getJpaRepository().findAll(specification,Sort.unsorted());
    }

    /**
     * 获取指定条数
     * @param specification 条件
     * @param sort 排序
     * @param pageNo 分页
     * @param pageSize 分页
     * @return
     */
    public List<T> findAll(@Nullable Specification<T> specification, @Nullable Sort sort, int pageNo, int pageSize){
        return findAll(specification, PageRequest.of(pageNo-1, pageSize, sort)).getContent();
    }

    /**
     * 分页
     * @param specification 条件
     * @param pageable 分页
     * @return
     */
    public  Page<T> findAll(@Nullable Specification<T> specification, @NotNull Pageable pageable){
        return getJpaRepository().findAll(specification, pageable);
    }

    /**
     * 获取总数
     * @param specification 条件
     * @return
     */
    public long count(@Nullable Specification<T> specification){
        return getJpaRepository().count(specification);
    }


    /**
     * 验证字段是否重复
     * @param fieldName 字段名
     * @param value 值
     * @param id id
     * @return
     */
    public boolean validValueDuplicate(@NotBlank String fieldName, @Nullable Object value,  @Nullable Object id){

        return findAll((root, query, criteriaBuilder) -> {
            List<Predicate> list = new ArrayList<>();
            list.add(criteriaBuilder.equal(root.get(fieldName), value));
            if(Objects.nonNull(id)){
                list.add(criteriaBuilder.notEqual(root.get("id"), id));
            }
            return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
        }).isEmpty();
    }

    @Override
    public Optional<T> findOneByField(@NotBlank String fieldName, Object value){
        if(Objects.isNull(value)){
            return getJpaRepository().findOne((root, query, criteriaBuilder) -> criteriaBuilder.isNull(getColumnPath(root,fieldName)));
        }
        return getJpaRepository().findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(getColumnPath(root,fieldName), value));
    }


    @Override
    public List<T> findByField(@NotBlank String fieldName, Object value) {
        if(Objects.isNull(value)){
            return getJpaRepository().findAll((root, query, criteriaBuilder) -> criteriaBuilder.isNull(getColumnPath(root,fieldName)));
        }
        return getJpaRepository().findAll((root, query, criteriaBuilder) -> criteriaBuilder.equal(getColumnPath(root,fieldName), value));
    }

    /**
     * 获取jpa 实体操作
     *
     * @return jpa
     */
    protected abstract JpaRepositoryImplementation<T, String> getJpaRepository();


    @Override
    public long count(List<SpecConditionDTO> specConditions) {
        return 0;
    }

    @Override
    protected Class<?> getAddClass() {
        throw new IllegalArgumentException("新增类型未配置");
    }

    @Override
    protected Class<? extends EditPDTO> getEditClass() {
        throw new IllegalArgumentException("编辑类型未配置");
    }

    @Override
    protected Class<? extends BaseExcelModalDTO> getExcelClass() {
        throw new IllegalArgumentException("导出类型未配置");
    }




    @Transactional
    protected <V extends BaseExcelModalDTO> List<T> excelDTOToEntity(List<V> list){
        String idField = getExcelDTOToEntityIdField();
        if(!StringUtils.hasText(idField)){
            return super.excelDTOToEntity(list);
        }
        List<T> result = new ArrayList<>(list.size());
        for (int i = 0; i < list.size(); i++) {
            V v = list.get(i);
            List<T> entityList = findByField(idField, ReflectionUtils.getFieldValue(v, idField));
            if(CollectionUtils.isEmpty(entityList)){
                T t = AkUtils.copyProperties(v, getEntityClass());
                excelFillEmbedId(v, t);
                result.add(t);

            }else if(entityList.size() == 1){
                T next = entityList.iterator().next();
                AkUtils.copyProperties(v, next);
                excelFillEmbedId(v, next);
                result.add(next);
            }else {
                throw new IllegalArgumentException("第" + v.getRowNum() + "条数据错误,系统存在"+entityList.size()+"条");
            }

        }
        return result;
    }

    protected String getExcelDTOToEntityIdField(){
        return "";
    }

    protected String getExcelDTOToEntityEmbedIdsField(){
        return "";
    }
    protected <V extends BaseExcelModalDTO> void excelFillEmbedId(V v, T t)  {
        String excelDTOToEntityEmbedIdsField = getExcelDTOToEntityEmbedIdsField();
        if(!StringUtils.hasText(excelDTOToEntityEmbedIdsField)) {
            return;
        }
        Set<String> embedIdSet = StringUtils.commaDelimitedListToSet(excelDTOToEntityEmbedIdsField);
        for (String embedId : embedIdSet) {
            List<AkUtils.MegerFieldName> megerFieldSourceName = AkUtils.findMegerFieldSourceName(embedId);
            AkUtils.MegerFieldName match = null;
            for (AkUtils.MegerFieldName megerFieldName : megerFieldSourceName) {
                String megerName = megerFieldName.getMegerName();
                String sourceName = megerFieldName.getSourceName();
                PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(t.getClass(), sourceName);
                // 此属性无写属性
                if (Objects.isNull(sourcePd) || Objects.isNull(sourcePd.getWriteMethod()) || Objects.isNull(sourcePd.getReadMethod())) {
                    continue;
                }
                Method readMethod = sourcePd.getReadMethod();
                Class<?> returnType = readMethod.getReturnType();
                if (Collection.class.isAssignableFrom(returnType)) {
                    returnType = ResolvableType.forMethodReturnType(readMethod).getGeneric(0).resolve();
                }
                PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(returnType, megerName);
                if (Objects.isNull(propertyDescriptor) || Objects.isNull(propertyDescriptor.getReadMethod())|| Objects.isNull(propertyDescriptor.getWriteMethod())) {
                    continue;
                }
                PropertyDescriptor embedIdPropertyDescriptor = BeanUtils.getPropertyDescriptor(v.getClass(), embedId);
                if (Objects.isNull(embedIdPropertyDescriptor) || Objects.isNull(embedIdPropertyDescriptor.getReadMethod())|| Objects.isNull(embedIdPropertyDescriptor.getWriteMethod())) {
                    continue;
                }
                match = megerFieldName;
                break;
            }
            if(match == null){
                throw new IllegalArgumentException("第" + v.getRowNum() + "条数据错误,属性"+embedId+"不匹配");
            }
            Object fieldValue = ReflectionUtils.getFieldValue(v, embedId);
            if(fieldValue == null){
                continue;
            }
            String megerName = match.getMegerName();
            String sourceName = match.getSourceName();
            PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(t.getClass(), sourceName);
            Method readMethod = sourcePd.getReadMethod();
            Class<?> entityType = readMethod.getReturnType();
            if (Collection.class.isAssignableFrom(entityType)) {
                entityType = ResolvableType.forMethodReturnType(readMethod).getGeneric(0).resolve();
            }
            String[] beanNamesForType = ApplicationHolder.getApplicationContext()
                    .getBeanNamesForType(ResolvableType.forClassWithGenerics(BaseService.class, entityType));
            if(beanNamesForType.length>1){
                throw new IllegalArgumentException("BaseService<"+entityType.getSimpleName()+">存在"+beanNamesForType.length+"个");
            }else if(beanNamesForType.length == 0){
                throw new IllegalArgumentException("BaseService<"+entityType.getSimpleName()+">不存在");
            }
            BaseService bean = (BaseService) ApplicationHolder.getApplicationContext().getBean(beanNamesForType[0]);
            if (Collection.class.isAssignableFrom(readMethod.getReturnType())) {
                Set<String> fieldValues = StringUtils.commaDelimitedListToSet(String.valueOf(fieldValue));
                fieldValues = fieldValues.stream().filter(m->StringUtils.hasText(m)).map(m->m.trim()).collect(Collectors.toSet());
                List<?> list  = bean.findAll(Arrays.asList(new SpecConditionDTO("in", String.valueOf(fieldValue), megerName)));
                if(list.size() != fieldValues.size()){
                    throw new IllegalArgumentException("第" + v.getRowNum() + "条数据错误,属性"+embedId+" 数据:"+fieldValue+"不匹配");
                }
                if(readMethod.getReturnType() == Set.class){
                    try {
                        sourcePd.getWriteMethod().invoke(t, new HashSet<>(list));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }else if(readMethod.getReturnType() == List.class){
                    try {
                        sourcePd.getWriteMethod().invoke(t, new ArrayList<>(list));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }else {
                List<?> list  = bean.findByField( megerName, fieldValue);
                if(list.size() == 0){
                    throw new IllegalArgumentException("第" + v.getRowNum() + "条数据错误,属性"+embedId+" 数据:"+fieldValue+"不存在");
                }else if(list.size()>1){
                    throw new IllegalArgumentException("第" + v.getRowNum() + "条数据错误,属性"+embedId+" 数据:"+fieldValue+"找到"+list.size()+"个");
                }
                try {
                    sourcePd.getWriteMethod().invoke(t, list.iterator().next());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

            }

        }

    }


}
